contents

1. 개요 및 탄생 배경


2. 핵심 원칙


3. 구조와 분리

구조 시각화

         +--------------------------+
         |        드라이버 어댑터       |
         +--------------------------+
         |         드라이버 포트        |
         +--------------------------+

       [육각형 애플리케이션 코어]
         +--------------------------+
         |       드리븐 포트         |
         +--------------------------+
         |      드리븐 어댑터        |
         +--------------------------+

4. 주요 장점


5. 일반 구현 단계

  1. 코어 설계:
    • 비즈니스/도메인 로직을 순수 객체/클래스로 작성.
    • 포트를 인터페이스로 정의(예: OrderRepository, EmailSender).
  2. 드라이버 어댑터 구현:
    • 웹 컨트롤러, CLI, 배치 등을 구현하여 드라이버 포트 호출.
  3. 드리븐 어댑터 구현:
    • DB, 외부 API, 메시지 브로커 등 기술 구체 구현을 드리븐 포트에 맞춰서 구현.
  4. 의존성 주입:
    • 어댑터와 포트를 어플리케이션 시작 시 연결(Dependency Injection).
    • 코어는 실제 구현체에 대한 정보 없이 인터페이스만 사용.

6. 실제 예시


7. 활용 및 고려사항


요약:
헥사고날 아키텍처는 핵심 도메인/비즈니스 로직을 모든 외부 기술로부터 완벽하게 분리하여, 포트(인터페이스)와 어댑터(구현체)를 통해만 입출력을 처리합니다. 이 구조는 유연성과 테스트 용이성, 인프라 변화에 대한 견고함을 제공합니다.


이제 간단한 java & spring 프로젝트로 예를 만들어 보겠습니다.


프로젝트 구조 예시

src/main/java/com/example/hexagonal/
├── domain/
│     ├── model/                 // 핵심 도메인 모델
│     │     └── Order.java
│     └── ports/                 // 도메인 포트(인터페이스)
│           ├── incoming/         // 드라이버 포트 - 애플리케이션 진입점
│           │     └── OrderServicePort.java
│           └── outgoing/         // 드리븐 포트 - 외부 시스템과 통신하는 인터페이스
│                 └── OrderRepositoryPort.java
├── application/
│     └── service/               // 드라이버 포트 구현, 유스케이스
│           └── OrderService.java
├── infrastructure/
│     ├── persistence/           // 드리븐 어댑터, DB 구현체
│     │     └── OrderRepositoryImpl.java
│     └── web/                   // 드라이버 어댑터, REST 컨트롤러
│           └── OrderController.java
├── HexagonalApplication.java    // Spring Boot 메인 클래스

1. 도메인 계층 - 모델 및 포트

// domain/model/Order.java
public class Order {
    private Long id;
    private String product;
    private int quantity;
    // 생성자, getter, setter 등
}

// domain/ports/incoming/OrderServicePort.java
import java.util.List;

public interface OrderServicePort {
    List<Order> getAllOrders();
    Order placeOrder(Order order);
}

// domain/ports/outgoing/OrderRepositoryPort.java
import java.util.List;

public interface OrderRepositoryPort {
    List<Order> findAll();
    Order save(Order order);
}

2. 애플리케이션 계층 - 유스케이스 구현

// application/service/OrderService.java
import com.example.hexagonal.domain.model.Order;
import com.example.hexagonal.domain.ports.incoming.OrderServicePort;
import com.example.hexagonal.domain.ports.outgoing.OrderRepositoryPort;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class OrderService implements OrderServicePort {
    private final OrderRepositoryPort orderRepository;

    public OrderService(OrderRepositoryPort orderRepository) {
        this.orderRepository = orderRepository;
    }

    @Override
    public List<Order> getAllOrders() {
        return orderRepository.findAll();
    }

    @Override
    public Order placeOrder(Order order) {
        // 비즈니스 로직 추가 가능
        return orderRepository.save(order);
    }
}

3. 인프라 계층 - 어댑터 구현

// infrastructure/persistence/OrderRepositoryImpl.java
import com.example.hexagonal.domain.model.Order;
import com.example.hexagonal.domain.ports.outgoing.OrderRepositoryPort;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;

@Repository
public interface JpaOrderRepository extends JpaRepository<Order, Long> {}

@Repository
public class OrderRepositoryImpl implements OrderRepositoryPort {
    private final JpaOrderRepository jpaOrderRepository;

    public OrderRepositoryImpl(JpaOrderRepository jpaOrderRepository) {
        this.jpaOrderRepository = jpaOrderRepository;
    }

    @Override
    public List<Order> findAll() {
        return jpaOrderRepository.findAll();
    }

    @Override
    public Order save(Order order) {
        return jpaOrderRepository.save(order);
    }
}
// infrastructure/web/OrderController.java
import com.example.hexagonal.domain.model.Order;
import com.example.hexagonal.domain.ports.incoming.OrderServicePort;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping("/orders")
public class OrderController {
    private final OrderServicePort orderService;

    public OrderController(OrderServicePort orderService) {
        this.orderService = orderService;
    }

    @GetMapping
    public List<Order> getOrders() {
        return orderService.getAllOrders();
    }

    @PostMapping
    public Order createOrder(@RequestBody Order order) {
        return orderService.placeOrder(order);
    }
}

4. Spring Boot 메인 클래스

// HexagonalApplication.java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class HexagonalApplication {
    public static void main(String[] args) {
        SpringApplication.run(HexagonalApplication.class, args);
    }
}

요약:

이렇게 헥사고날 아키텍처는 핵심 로직과 외부 기술을 완벽히 분리하여 높은 유연성과 테스트 용이성을 제공합니다.


사실 이렇게 봐선 클린 아키텍처와 헥사고날 아키텍처의 차이를 이해하기는 쉽지 않습니다. 솔직히 별로 다르지도 않구요... 좀 더 이해를 돕기 위해 찾아봤습니다.


주요 차이점

1. 구조 및 레이어


2. 모듈화 및 초점


3. 유연성 대 구조화


요약 표

평가 항목 헥사고날 아키텍처 클린 아키텍처
레이어 구조 엄격한 레이어 구조 없음 명확한 동심원 레이어 구조
의존성 방향 코어 <-> 포트/어댑터 엄격히 안쪽으로만 의존
코어 조직 상대적으로 자유로움 도메인, 유스케이스 등으로 세분화
유연성 단순하고 빠름 구조적, 유지보수·확장 용이
적용 대상 마이크로서비스, 다양한 통합 환경 대규모, 복잡하고 모듈화 필요한 시스템
초점 코어 독립성 및 통합 가변성 모듈화, 테스트 용이성 및 규칙 준수
테스트 용이성 매우 높음 매우 높음

결론

클린 아키텍처는 헥사고날 아키텍처에 내부 레이어링과 의존성 방향 규칙을 더 명확히 한 발전형으로 볼 수 있습니다. 헥사고날은 핵심 비즈니스 로직과 외부 통합을 인터페이스로 분리해 단순하고 유연한 반면, 클린은 이 모델을 확장해 규모 있는 시스템에서도 관리 및 확장이 쉽도록 설계되었습니다.

references